package com.manong.config.security.filter;

import com.manong.config.handler.LoginFailureHandler;
import com.manong.config.redis.RedisService;
import com.manong.config.security.Service.CustomerUserDetailsService;
import com.manong.config.security.exception.CustomerAuthenticationException;
import com.manong.utils.JwtUtils;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * token过滤器类
 * @author 李茂鑫
 * @Date 2025/1/15 10:22
 * @Descripition
 */
@Data
@Component
//OncePerRequestFilter：Spring Security 提供的基类，确保过滤器每次请求只被执行一次。它内部实现了 doFilterInternal 方法，用户只需要重写该方法即可。
public class CheckTokenFilter extends OncePerRequestFilter {
    @Resource
    private JwtUtils jwtUtils;  //处理JWT相关操作
    @Resource
    private CustomerUserDetailsService customerUserDetailsService; //加载用户的详细信息
    @Resource
    private LoginFailureHandler loginFailureHandler; //处理认证失败
    @Resource
    private RedisService redisService; //用于和redis交互，判断token是否有效
    //获取登录请求地址
    @Value("${request.login.url}")
    private String loginUrl;



    /**
     * doFilterInternal：该方法在每次请求时被调用，逻辑如下：
     * 获取请求的 URI 地址 (request.getRequestURI())，并判断是否是登录请求或静态资源请求（如图片）。
     * 如果是静态资源请求（如图片），直接调用 doFilter 进行链式处理，不进行 token 验证。
     * 如果不是登录请求，调用 validateToken 方法验证 token。
     * 如果验证失败（AuthenticationException），则交由 loginFailureHandler 处理认证失败逻辑。
     * 最后，无论是否验证通过，都会调用 doFilter 放行请求，进入过滤链的下一个过滤器。
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            //获取当前请求的地址
            String url = request.getRequestURI();
            //如果当前请求地址不是登录请求或者静态图片资源，则需要token验证
            if (url.contains("/img/")) {
                doFilter(request, response, filterChain);
            } else if (!url.equals(loginUrl))
            {
                this.validateToken(request);
            }
        } catch (org.springframework.security.core.AuthenticationException e) {
            loginFailureHandler.onAuthenticationFailure(request, response, e);
        }
        //登录请求地址，直接放行
        doFilter(request, response, filterChain);
    }

    /**
     * 验证token
     *validateToken：验证 token 的有效性，执行以下步骤：
     * 从请求中获取 token：先从请求头部 (request.getHeader("token")) 获取 token，如果不存在，再从请求参数中获取 (request.getParameter("token"))。
     * 检查 token 是否存在：如果 token 不存在，则抛出 CustomerAuthenticationException 异常，提示 "token不存在"。
     * 检查 Redis 中是否存在 token：将 token 拼接成 token_ 前缀存储的 Redis 键值（如：token_abc123），从 Redis 中获取 token。如果 Redis 中没有该 token，抛出 "用户登录已过期" 异常。
     * 检查 token 是否匹配 Redis 中的 token：如果请求中的 token 和 Redis 中的 token 不一致，则说明该 token 已失效，抛出 "token验证失败" 异常。
     * 从 token 中解析出用户名：通过 jwtUtils.getUserNameFromToken(token) 获取用户名称。如果解析失败，抛出 "token解析失败" 异常。
     * 加载用户信息：通过 customerUserDetailsService.loadUserByUsername(userName) 加载用户的详细信息（UserDetails），并进行空值检查。如果没有找到该用户，抛出 "token验证失败" 异常。
     * 构建认证对象：使用 UsernamePasswordAuthenticationToken 创建认证对象，并设置用户的权限（userDetails.getAuthorities()）。
     * 设置到 Spring Security 上下文中：通过 SecurityContextHolder.getContext().setAuthentication(authenticationToken) 将认证信息存入 Spring Security 的上下文中，表示当前请求已经认证通过。
     * @param request
     */
    private void validateToken(HttpServletRequest request) throws AuthenticationException {
        //从头部信息中获取token
        String token = request.getHeader("token");
        //如果请求头部中没有token，则从请求参数中获取token
        if (ObjectUtils.isEmpty(token)) {
            token = request.getParameter("token");
        }
        //如果请求参数中也没有token，则抛出异常
        if (ObjectUtils.isEmpty(token)) {
            throw new CustomerAuthenticationException("token不存在");
        }
        //判断redis中是否存在token
        String tokenKey = "token_" + token;
        String redisToken = redisService.get(tokenKey);
        //如果redis中不存在token，说明该token已经失效，抛出异常
        if (ObjectUtils.isEmpty(redisToken)) {
            throw new CustomerAuthenticationException("用户登录已过期");
        }
        //如果token和redis中的token不一致，说明该token已经失效，抛出异常
        if (!token.equals(redisToken)) {
            throw new CustomerAuthenticationException("token验证失败");
        }
        //如果存在token，则从token中获取用户信息
        String userName = jwtUtils.getUserNameFromToken(token);
        //如果用户信息为空，说明token已经失效，抛出异常
        if (ObjectUtils.isEmpty(userName)) {
            throw new CustomerAuthenticationException("token解析失败");
        }
        //获取用户信息
        UserDetails userDetails = customerUserDetailsService.loadUserByUsername(userName);
        //判断用户信息是否为空
        if (userDetails == null) {
            throw new CustomerAuthenticationException("token验证失败");
        }
        //创建身份对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        //设置到spring security上下文
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
}
